package alocador.gerenciadores;

import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.Set;

import alocador.entidades.Evento;
import alocador.entidades.salas.SalaConcreta;
import alocador.exceptions.EventoInvalidoException;
import alocador.exceptions.EventoNaoExistenteException;
import alocador.exceptions.SalaInvalidaException;
import alocador.persistencia.PersistenciaFacade;
import alocador.persistencia.Gravavel;
import alocador.util.VerificaInvalido;

/**
 * Classe que ira Gerenciar as Alocacoes do Sistema
 */
public class GerenciadorDeAlocacao implements Gravavel {

	private static GerenciadorDeAlocacao instanciaUnica = null;
	private HashMap<String, ArrayList<Evento>> alocacoes;
	private static String fileName = "GerAloc";

	@Override
	public void zerarDadosInternos() {
		alocacoes = new HashMap<String, ArrayList<Evento>>();
	}

	@Override
	public String getFileName() {
		return GerenciadorDeAlocacao.fileName;
	}

	/**
	 * Metodo que implementa o Padrao Singleton para o Gerenciador de Alocacao
	 * 
	 * @return Instancia Unica do Gerenciador de Alocacao
	 */
	public static GerenciadorDeAlocacao getInstance() {

		if (instanciaUnica == null) {

			GerenciadorDeAlocacao gda = (GerenciadorDeAlocacao) PersistenciaFacade
					.recuperarArquivo(fileName);

			if (gda == null)
				gda = new GerenciadorDeAlocacao();

			instanciaUnica = gda;
		}

		return instanciaUnica;
	}

	/**
	 * Construtor pega a instancia do gerenciador de Salas e a instancia do<br>
	 * gerenciador de Eventos para poder efetuar as devidas alocacoes.<br>
	 * Construtor eh privado por ser um singleton.
	 */
	private GerenciadorDeAlocacao() {
		zerarDadosInternos();
	}

	/**
	 * Metodo responsavel por alocar um evento a uma sala.
	 * 
	 * @param idEvento
	 *            Representa o identificador do evento
	 * 
	 * @param idSala
	 *            Representa o identificador da sala
	 *            
	 * @throws SalaInvalidaException
	 * 			   - Ao verificar a Finalidade podemos lancar esta excessao -
	 * 			   - Ao verificar o tipo podemos lancar esta excessao -
	 * 			   - Ao verificar a sala aloca
	 *  
	 * @throws EventoInvalidoException
	 * 			   - Quando a Sala/Evento nao existe -
	 *  
	 */
	public void alocarEvento(String idEvento, String idSala)
			throws EventoInvalidoException, SalaInvalidaException {

		if ((VerificaInvalido.basico(idEvento) || VerificaInvalido
				.basico(idSala)))
			throw new EventoInvalidoException("Sala/Evento nao existe.");

		SalaConcreta sala = null;
		Evento evento = null;

		try {
			sala = GerenciadorDeSalas.getInstance().getSala(idSala);
			evento = GerenciadorDeEventos.getInstance().getEvento(idEvento);
		} catch (Exception e) {
			throw new EventoInvalidoException("Sala/Evento nao existe.");
		}

		if (!alocacoes.containsKey(idSala))
			alocacoes.put(idSala, new ArrayList<Evento>());

		verificaFinalidade(sala, evento);
		verificaTipo(sala, evento);
		verificaEventoAlocado(evento);
		verificaSalaAlocada(sala, evento);

		alocacoes.get(idSala).add(evento);
	}

	/**
	 * Metodo responsavel por verificar se a finalidade de uma sala esta
	 * coerente com o evento que sera alocado a ela, ou se pode realmente alocar
	 * um evento
	 * 
	 * @param sala
	 *            Representa a sala onde sera alocado o evento
	 * 
	 * @param evento
	 *            Representa o evento que sera alocado na sala
	 *            
	 * @throws SalaInvalidaException
	 * 			   - Escritorios nao sao escalonaveis -
	 * 			   - Laboratorios abertos nao sao escalonaveis -
	 * 			   - Salas de Conferencia do tipo Normal nao sao escalonaveis
	 * 			   para eventos repetitivos -
	 * 			    
	 */
	private void verificaFinalidade(SalaConcreta sala, Evento evento)
			throws SalaInvalidaException {
		final int UMA_REPETICAO = 1;

		if (sala.getClassificacao().equals("Escritorio"))
			throw new SalaInvalidaException("Escritorios nao sao escalonaveis.");

		if (sala.getClassificacao().equals("Laboratorio")) {

			if (!sala.isAlocavel())
				throw new SalaInvalidaException(
						"Laboratorios abertos nao sao escalonaveis.");
		}

		if (sala.getClassificacao().equals("Sala de Conferencia")) {
			if (sala.getSubTipo().equals("Normal"))
				if (evento.getRepeticoes() > UMA_REPETICAO)
					throw new SalaInvalidaException(
						"Salas de Conferencia do tipo Normal nao " +
						"sao escalonaveis para eventos repetitivos.");
		}
	}

	/**
	 * Metodo responsavel por verificar se o tipo de uma sala esta coerente com
	 * a area do evento que sera alocado a ela
	 * 
	 * @param sala
	 *            Representa a sala onde sera alocado o evento
	 * 
	 * @param evento
	 *            Representa o evento que sera alocado na sala
	 *            
	 * @throws EventoInvalidoException
	 * 			   - Quando a area do evento passado nao eh compativel com o
	 * 			   subtipo da sala -
	 * 
	 * @throws SalaInvalidaException
	 *             - Quando o tipo da sala passada nao eh compativel com a area
	 *             do evento desejado -
	 */
	private void verificaTipo(SalaConcreta sala, Evento evento)
			throws SalaInvalidaException, EventoInvalidoException {

		if ((sala.getSubTipo().equals("Biologia")) ||
			(sala.getSubTipo().equals("Fisica")) ||
			(sala.getSubTipo().equals("Quimica"))) {

			if (!evento.getArea().toString().toLowerCase().equals(
					sala.getSubTipo().toLowerCase())){
				
				throw new EventoInvalidoException(
					"Sala exclusiva para a area de " + sala.getSubTipo() + ".");
			}
		}
	}

	/**
	 * Metodo responsavel por verificar se um evento ja foi alocado
	 * anteriormente
	 * 
	 * @param idEvento
	 *            Representa o id do evento
	 * 
	 * @throws EventoInvalidoException
	 *             - Caso o evento ja tenha sido alocado anteriormente -
	 */
	private void verificaEventoAlocado(Evento evento)
			throws EventoInvalidoException {

		for (ArrayList<Evento> eventosSala : alocacoes.values()) {
			if (eventosSala.contains(evento))
				throw new EventoInvalidoException(
						"O Evento ja foi alocado anteriormente.");
		}
	}

	/**
	 * Metodo responsavel por verificar se uma sala esta disponivel em
	 * determinado horario solicitado e verifica se o evento vai ser alocado em
	 * um final de semana
	 * 
	 * @param sala
	 *            Representa a sala onde o evento sera alocado
	 * 
	 * @param evento
	 *            Representa o evento que sera alocadoa
	 * 
	 * @throws SalaInvalidaException
	 *             - Caso a sala nao esteja disponivel em determinado horario -
	 *             - Caso o horario do evento esteja em um final de semana -
	 */
	private void verificaSalaAlocada(SalaConcreta sala, Evento evento)
			throws SalaInvalidaException {

		for (Evento e : alocacoes.get(sala.getNome()))
			if (evento.temConflitoCom(evento, e))
				throw new SalaInvalidaException(
						"A sala nao esta disponivel neste horario.");

		if (VerificaInvalido.ehFinalDeSemana(evento.getInicio()))
			throw new SalaInvalidaException("As salas nao sao alocadas"
					+ " nos fins de semana.");
	}

	/**
	 * Metodo responsavel por localizar um evento, por algum de seus atributos,
	 * nas alocacoes do sistema
	 * 
	 * @param atributo
	 *            Representa por qual atributo do evento a busca sera feita,
	 *            podendo ser pelo Nome, Contato ou Horario.
	 * 
	 * @param valor
	 *            Representa o conteudo do atributo que esta sendo procurado
	 * 
	 * @return Uma String contendo o id da sala onde o evento esta alocado e o
	 *         id do evento, neste formato: (idSala):(idEvento)
	 * 
	 * @throws EventoNaoExistenteException
	 *             - Caso a String nomeDoEvento nao seja valida -
	 */
	public String localizaEvento(String atributo, String valor)
			throws EventoNaoExistenteException {

		ArrayList<Evento> eventos = null;

		if (VerificaInvalido.basico(atributo))
			throw new EventoNaoExistenteException("Entrada Invalida");

		if (VerificaInvalido.basico(valor))
			throw new EventoNaoExistenteException("Entrada Invalida");

		try {

			if (atributo.equals("nome"))
				eventos = GerenciadorDeEventos.getInstance().
													getEventoPorNome(valor);
			else if (atributo.equals("contato"))
				eventos = GerenciadorDeEventos.getInstance().
													getEventoPorContato(valor);
			else if (atributo.equals("horario"))
				eventos = GerenciadorDeEventos.getInstance().
													getEventoPorHorario(valor);
			
		} catch (Exception e) {
			return "Nenhum evento encontrado.";
		}
		return eventosOrdenados(eventos);
	}

	/**
	 * Metodo responsavel por desalocar um evento de uma sala
	 * 
	 * @param idEvento
	 *            Representa o id do evento a ser desalocado
	 * 
	 * @throws EventoInvalidoException
	 *             - Quando o id do evento recebido nao existe - 
	 *             - Quando o id representa uma sala ainda nao alocada -
	 */
	public void desalocarEvento(String idEvento)
			throws EventoNaoExistenteException, EventoInvalidoException {

		if (VerificaInvalido.basico(idEvento))
			throw new EventoInvalidoException("Evento nao existe.");

		Evento evento = null;

		try {
			evento = GerenciadorDeEventos.getInstance().getEvento(idEvento);
		} catch (Exception e) {
			throw new EventoInvalidoException("Evento nao existe.");
		}

		Set<String> idSalas = alocacoes.keySet();

		for (String idSala : idSalas) {
			for (Evento e : alocacoes.get(idSala)) {
				if (evento.equals(e)) {
					alocacoes.get(idSala).remove(evento);
					return;
				}
			}
		}

		throw new EventoInvalidoException("O evento nao esta alocado.");
	}

	/**
	 * Metodo responsavel por desalocar todos os eventos de uma sala
	 * 
	 * @param idSala
	 *            Representa o id de uma sala
	 *            
	 * @throws SalaInvalidaException
	 *             - Quando o id da sala recebido eh invalido -
	 */
	public void desalocarSala(String idSala) throws SalaInvalidaException {
		
		if (VerificaInvalido.basico(idSala))
			throw new SalaInvalidaException("Sala Invalida");

		alocacoes.remove(idSala);
	}

	/**
	 * Metodo responsavel por retornar o id da sala onde um evento esta alocado
	 * 
	 * @param evento
	 *            Representa o evento que precisamos saber em qual sala ele esta
	 *            alocado
	 * 
	 * @return Uma string contendo o idSala caso o evento esteja alocado em uma
	 *         se nao retorna null
	 */
	private String getSala(Evento evento) {

		Set<String> idSalas = alocacoes.keySet();

		for (String idSala : idSalas) {
			for (Evento e : alocacoes.get(idSala)) {
				if (evento.equals(e))
					return idSala;
			}
		}
		return null;
	}

	/**
	 * Metodo responsavel por ordenar os eventos alocados
	 * 
	 * @param eventos
	 *            Representa os eventos alocados numa lista
	 * 
	 * @return Uma String contendo os eventos alocados em ordem alfabetica
	 */
	private String eventosOrdenados(ArrayList<Evento> eventos) {

		final int FORMATACAO_DE_SAIDA = 2;
		final int CARACTER_INICIAL = 0;
		String saida = "";
		ArrayList<String> eventosOrdenados = new ArrayList<String>();

		for (Evento evento : eventos) {
			String idSala = getSala(evento);
			if (idSala != null)
				eventosOrdenados.add(idSala + ":" + evento.getId());
		}

		if (eventosOrdenados.isEmpty())
			return "Nenhum evento encontrado.";
		
		Collections.sort(eventosOrdenados);

		for (String evento : eventosOrdenados) {
			saida += evento + ", ";
		}

		return saida == "" ? "Nenhum evento encontrado." : saida.substring(
				CARACTER_INICIAL, saida.length() - FORMATACAO_DE_SAIDA);
	}

}